package org.apache.ocean.main;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.index.Term;
import org.apache.ocean.main.DiskIndex.DiskIndexSnapshot;
import org.apache.ocean.main.Index.IndexSnapshot;
import org.apache.ocean.main.IndexDocuments.IndexDocument;
import org.apache.ocean.main.RamIndex.RamIndexSnapshot;
import org.apache.ocean.main.Snapshot.SnapshotInfo.IndexInfo;
import org.apache.ocean.main.TransactionSystem.Category;
import org.apache.ocean.main.WriteableMemoryIndex.MemoryIndexSnapshot;
import org.apache.ocean.main.fields.FieldType;
import org.apache.ocean.main.search.cache.CacheKey;
import org.apache.ocean.main.util.CElement;
import org.apache.ocean.main.util.Constants;
import org.apache.ocean.main.util.IndexUtil;
import org.apache.ocean.main.util.XMLUtil;
import org.jdom.Element;

public class Snapshot {
	private BigDecimal id;
	private Map<Long, IndexSnapshot> indexSnapshotMap;
	private MemoryIndexSnapshot writeableSnapshot;
	private Category category;
	private IndexSchema.Category schemaCategory;
	private Map<IndexReader,IndexSnapshot> readerMap;
	
	public Snapshot(Long snapshotID, int minorVersion, MemoryIndexSnapshot writeableSnapshot, Collection<IndexSnapshot> indexSnapshots, Category category, IndexSchema.Category schemaCategory) throws IOException {
		id = toId(snapshotID, minorVersion);
		this.writeableSnapshot = writeableSnapshot;
		this.category = category;
		this.schemaCategory = schemaCategory;
		assert snapshotIdsMatch(indexSnapshots);
		indexSnapshotMap = new HashMap<Long, IndexSnapshot>(indexSnapshots.size());
		readerMap = new HashMap<IndexReader, IndexSnapshot>(indexSnapshots.size());
		for (IndexSnapshot indexSnapshot : indexSnapshots) {
			indexSnapshotMap.put(indexSnapshot.getIndex().getId(), indexSnapshot);
			readerMap.put(indexSnapshot.getIndexReader(), indexSnapshot);
		}
	}
	
	public IndexSnapshot getIndexSnapshot(IndexReader indexReader) {
		return readerMap.get(indexReader);
	}
	
	public IndexSchema.Category getIndexSchemaCategory() {
		return schemaCategory;
	}
	
	public List<RamIndexSnapshot> getRamIndexSnapshots() {
		List<RamIndexSnapshot> ramIndexSnapshots = new ArrayList<RamIndexSnapshot>();
		for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
			if (indexSnapshot instanceof RamIndexSnapshot) {
				ramIndexSnapshots.add((RamIndexSnapshot)indexSnapshot);
			}
		}
		return ramIndexSnapshots;
	}
	
	public IndexReader[] getIndexReaders() {
		IndexReader[] indexReaders = new IndexReader[indexSnapshotMap.size()];
		int i = 0;
		for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
			indexReaders[i] = indexSnapshot.getIndexReader();
			i++;
		}
		return indexReaders;
	}
	
	public IndexDocuments getDocuments(List<Long> docIds, FieldSelector fieldSelector, IndexSchema.Category schemaCategory) throws ParseException, IOException {
		IndexReader[] indexReaders = getIndexReaders();
		MultiReader multiReader = new MultiReader(indexReaders);
		FieldType fieldType = schemaCategory.getFieldType(Constants.ID);
		IndexDocuments indexDocuments = new IndexDocuments();
		for (Long docId : docIds) {
			String indexed = fieldType.objectToIndexed(docId);
			Term term = new Term(Constants.ID, indexed);
			int doc = IndexUtil.getTermDoc(term, multiReader);
			Document document = multiReader.document(doc, fieldSelector);
			IndexDocument indexDocument = new IndexDocument(document, schemaCategory);
			indexDocuments.add(indexDocument);
		}
		return indexDocuments;
	}
	
	public int getMaxDoc() {
		int maxDoc = 0;
		for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
			maxDoc += indexSnapshot.getIndexReader().maxDoc();
		}
		return maxDoc;
	}
	
	public Category getCategory() {
		return category;
	}
	
	public int getMinorVersion() {
		return getMinorVersion(id);
	}
	
	public static BigDecimal toId(Long snapshotId, int minorVersion) {
		StringBuilder builder = new StringBuilder();
		builder.append(snapshotId);
		builder.append(".");
		if (10 > minorVersion) builder.append("0");
		builder.append(minorVersion);
		BigDecimal value = new BigDecimal(builder.toString());
		return value;
	}
	
	public static int getMinorVersion(BigDecimal value) {
		value = value.subtract(new BigDecimal(value.longValue()));
		BigDecimal decimal = value.scaleByPowerOfTen(2);
		return decimal.intValue();
	}
	
	public static boolean snapshotIdsMatch(Collection<IndexSnapshot> indexSnapshots) {
		Long current = null;
		for (IndexSnapshot indexSnapshot : indexSnapshots) {
			if (current == null) {
				current = indexSnapshot.getSnapshotId();
			} else if (!current.equals(indexSnapshot.getSnapshotId())) {
				return false;
			}
		}
		return true;
	}
	
	public Snapshot(BigDecimal id, MemoryIndexSnapshot writeableSnapshot, Collection<IndexSnapshot> indexSnapshots, Category category) throws IOException {
		this.id = id;
		this.writeableSnapshot = writeableSnapshot;
		this.category = category;
		assert snapshotIdsMatch(indexSnapshots);
		indexSnapshotMap = new HashMap<Long, IndexSnapshot>();
		for (IndexSnapshot indexSnapshot : indexSnapshots) {
			indexSnapshotMap.put(indexSnapshot.getIndex().getId(), indexSnapshot);
		}
	}
  
	public SnapshotInfo getSnapshotInfo() {
		SnapshotInfo snapshotInfo = new SnapshotInfo(id, schemaCategory.getIndexSchema().getVersion());
		for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
			Index index = indexSnapshot.getIndex();
			String type = null;
			if (index instanceof DiskIndex) type = "disk";
			if (index instanceof WriteableMemoryIndex) type = "memory";
			IndexInfo indexInfo = new IndexInfo(index.getId(), type);
			snapshotInfo.add(indexInfo);
		}
		return snapshotInfo;
	}
	
	public static SnapshotInfo loadHighestSnapshotInfo(File directoryFile) throws Exception {
		TreeSet<BigDecimal> sortedSet = new TreeSet<BigDecimal>();
		for (File file : directoryFile.listFiles()) {
			if (!file.isDirectory()) {
				String name = file.getName();
				String str = "snapshot_";
				if (name.startsWith(str)) {
					String main = name.substring(str.length() - 1, name.lastIndexOf('.'));
					String[] split = StringUtils.split(main);
					Long snapshotId = new Long(split[0]);
					Integer version = new Integer(split[1]);
					sortedSet.add(new BigDecimal(snapshotId + "." + version));
				}
			}
		}
		BigDecimal highestId = sortedSet.last();
    if (highestId == null) return null;
    String fileName = getFileName(highestId);
    File file = new File(directoryFile, fileName);
    Element element = XMLUtil.parseElement(file);
    return new SnapshotInfo(element);
	}

	public static String getFileName(BigDecimal id) {
		String string = id.toString();
		String replaced = string.replace('.', '_');
		return "snapshot_" + replaced + ".xml";
	}

	public static class SnapshotInfo implements CElement {
		private BigDecimal id;
		private Long schemaVersion;
		private Map<Long, IndexInfo> indexInfos;
    
		public SnapshotInfo(BigDecimal id, Long schemaVersion) {
			this.id = id;
			this.schemaVersion = schemaVersion;
			indexInfos = new HashMap<Long, IndexInfo>();
		}
		
		public void add(IndexInfo indexInfo) {
			indexInfos.put(indexInfo.getId(), indexInfo);
		}
		
		public SnapshotInfo(Element element) {
			indexInfos = new HashMap<Long, IndexInfo>();
			id = new BigDecimal(element.getAttributeValue("id"));
			schemaVersion = XMLUtil.getAttributeLong("schemaversion", element);
			for (Element indexElement : XMLUtil.getChildren("index", element)) {
				IndexInfo indexInfo = new IndexInfo(indexElement);
				indexInfos.put(indexInfo.getId(), indexInfo);
			}
		}
		
		public Long getSchemaVersion() {
			return schemaVersion;
		}
		
		public Long getSnapshotId() {
			return id.longValue();
		}
		
		public BigDecimal getId() {
			return id;
		}
		
		public Collection<IndexInfo> getIndexInfos() {
			return indexInfos.values();
		}

		public SnapshotInfo(BigDecimal id, Map<Long, IndexInfo> indexInfos) {
			this.id = id;
			this.indexInfos = indexInfos;
		}

		public static class IndexInfo implements CElement {
			private Long id;
			private String type;
  
			public IndexInfo(Long id, String type) {
				this.id = id;
				this.type = type;
			}

			public IndexInfo(Element element) {
				id = XMLUtil.getAttributeLong("id", element);
				type = XMLUtil.getAttributeString("type", element);
			}

			public Long getId() {
				return id;
			}

			public String getType() {
				return type;
			}

			public Element toElement() {
				Element element = new Element("index");
				XMLUtil.setAttribute("id", id, element);
				XMLUtil.setAttribute("type", type, element);
				return element;
			}
		}

		public void writeTo(File file) throws IOException {
			Element element = toElement();
			XMLUtil.saveXML(element, file);
		}

		public Element toElement() {
			Element element = new Element("snapshot");
			XMLUtil.setAttribute("id", id, element);
			for (IndexInfo indexInfo : indexInfos.values()) {
				element.addContent(indexInfo.toElement());
			}
			return element;
		}
	}
  
	public Snapshot createMinor(List<Long> removeIndexIds, IndexSnapshot newIndexSnapshot) throws IOException {
		return createMinor(removeIndexIds, writeableSnapshot, newIndexSnapshot);
	}
	
	public Snapshot createMinor(List<Long> removeIndexIds, MemoryIndexSnapshot writeableSnapshot, IndexSnapshot newIndexSnapshot) throws IOException {
		HashMap<Long, IndexSnapshot> mapCopy = new HashMap<Long, IndexSnapshot>(indexSnapshotMap);
		for (Long indexid : removeIndexIds) {
			mapCopy.remove(indexid);
		}
		Long newIndexId = newIndexSnapshot.getIndex().getId();
		assert !mapCopy.containsKey(newIndexId);
		mapCopy.put(newIndexId, newIndexSnapshot);
		int minorVersion = getMinorVersion();
		Long snapshotID = getSnapshotId();
		BigDecimal newId = toId(snapshotID, minorVersion);
		Snapshot newSnapshot = new Snapshot(newId, writeableSnapshot, mapCopy.values(), category);
		return newSnapshot;
	}

	public List<DiskIndex> getDiskIndices() { 
    List<DiskIndex> diskIndices = new ArrayList<DiskIndex>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
    	indexSnapshot.getIndex();
    }
    return diskIndices;
	}

	public List<Index> getDeleteOnlyIndices() {
		HashMap<Long, IndexSnapshot> mapCopy = new HashMap<Long, IndexSnapshot>(indexSnapshotMap);
		mapCopy.remove(writeableSnapshot.getIndex().getId());
		List<Index> indices = new ArrayList<Index>();
		for (IndexSnapshot indexSnapshot : mapCopy.values()) {
			indices.add(indexSnapshot.getIndex());
		}
		return indices;
	}

	public MemoryIndexSnapshot getWriteableSnapshot() {
		return writeableSnapshot;
	}

	public Set<CacheKey> getCacheKeys() {
		Set<CacheKey> cacheKeys = new HashSet<CacheKey>();
		for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
			cacheKeys.addAll(indexSnapshot.getCacheKeys());
		}
		return cacheKeys;
	}

	public boolean containsIndex(long indexid) {
		return indexSnapshotMap.containsKey(indexid);
	}

	public List<DiskIndexSnapshot> getDiskIndexSnapshots() {
		List<DiskIndexSnapshot> diskIndexSnapshots = new ArrayList<DiskIndexSnapshot>();
		for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
			if (indexSnapshot instanceof DiskIndexSnapshot) {
				diskIndexSnapshots.add((DiskIndexSnapshot) indexSnapshot);
			}
		}
		return diskIndexSnapshots;
	}

	public Collection<IndexSnapshot> getIndexSnapshots() {
		return indexSnapshotMap.values();
	}

	public Long getSnapshotId() {
		return id.longValue();
	}

	public BigDecimal getId() {
		return id;
	}
}
